package com.nbstar.film.spider.font.woff.table.gsub;


import com.nbstar.film.spider.font.woff.AbstractOTFDataStream;
import com.nbstar.film.spider.font.woff.AbstractWoffTable;
import com.nbstar.film.spider.font.woff.ByteBufferOTFDataStream;
import com.nbstar.film.spider.font.woff.WoffFont;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * GSUB table
 * Created with IntelliJ IDEA.
 *
 * @author: wangxindong
 * @date: 2018/9/1-21:52
 */
@SuppressWarnings("all")
public class GlyphSubstitutionTable extends AbstractWoffTable {

    public static final String TAG = "GSUB";

    private LinkedHashMap<String, ScriptTable> scriptList;
    private FeatureRecord[] featureList;
    private LookupTable[] lookupList;

    private final Map<Integer, Integer> lookupCache = new HashMap<>();
    private final Map<Integer, Integer> reverseLookup = new HashMap<>();

    private String lastUsedSupportedScript;

    public GlyphSubstitutionTable(WoffFont woffFont) {
        super(woffFont);
    }

    @Override
    public void read(WoffFont woffFont, AbstractOTFDataStream stream) throws IOException {
        stream.seek(offset);
        byte[] fontData = stream.read(compLength);
        byte[] inflateFont = inflateFontData(fontData);
        AbstractOTFDataStream data = new ByteBufferOTFDataStream(inflateFont);

        long start = data.getCurrentPosition();
        int majorVersion = data.readUnsignedShort();
        int minorVersion = data.readUnsignedShort();
        int scriptListOffset = data.readUnsignedShort();
        int featureListOffset = data.readUnsignedShort();
        int lookupListOffset = data.readUnsignedShort();
        long featureVariationsOffset = -1L;
        if (minorVersion == 1L) {
            featureVariationsOffset = data.readUnsignedInt();
        }
        scriptList = readScriptList(data, scriptListOffset);
        featureList = readFeatureList(data, featureListOffset);
        lookupList = readLookupList(data, lookupListOffset);

    }

    private LookupTable[] readLookupList(AbstractOTFDataStream data, long offset) throws IOException {
        data.seek(offset);
        int lookupCount = data.readUnsignedShort();
        int[] lookups = new int[lookupCount];
        for (int i = 0; i < lookupCount; i++) {
            lookups[i] = data.readUnsignedShort();
        }
        LookupTable[] lookupTables = new LookupTable[lookupCount];
        for (int i = 0; i < lookupCount; i++) {
            lookupTables[i] = readLookupTable(data, offset + lookups[i]);
        }
        return lookupTables;
    }

    LookupTable readLookupTable(AbstractOTFDataStream data, long offset) throws IOException {
        data.seek(offset);
        LookupTable lookupTable = new LookupTable();
        lookupTable.lookupType = data.readUnsignedShort();
        lookupTable.lookupFlag = data.readUnsignedShort();
        int subTableCount = data.readUnsignedShort();
        int[] subTableOffets = new int[subTableCount];
        for (int i = 0; i < subTableCount; i++) {
            subTableOffets[i] = data.readUnsignedShort();
        }
        if ((lookupTable.lookupFlag & 0x0010) != 0) {
            lookupTable.markFilteringSet = data.readUnsignedShort();
        }
        lookupTable.subTables = new LookupSubTable[subTableCount];
        switch (lookupTable.lookupType) {
            case 1: // Single
                for (int i = 0; i < subTableCount; i++) {
                    lookupTable.subTables[i] = readLookupSubTable(data, offset + subTableOffets[i]);
                }
                break;
            default:
                // Other lookup types are not supported
                LOGGER.debug("Type " + lookupTable.lookupType + " GSUB lookup table is not supported and will be ignored");
        }
        return lookupTable;
    }

    LookupSubTable readLookupSubTable(AbstractOTFDataStream data, long offset) throws IOException {
        data.seek(offset);
        int substFormat = data.readUnsignedShort();
        switch (substFormat) {
            case 1: {
                LookupTypeSingleSubstFormat1 lookupSubTable = new LookupTypeSingleSubstFormat1();
                lookupSubTable.substFormat = substFormat;
                int coverageOffset = data.readUnsignedShort();
                lookupSubTable.deltaGlyphID = data.readSignedShort();
                lookupSubTable.coverageTable = readCoverageTable(data, offset + coverageOffset);
                return lookupSubTable;
            }
            case 2: {
                LookupTypeSingleSubstFormat2 lookupSubTable = new LookupTypeSingleSubstFormat2();
                lookupSubTable.substFormat = substFormat;
                int coverageOffset = data.readUnsignedShort();
                int glyphCount = data.readUnsignedShort();
                lookupSubTable.substituteGlyphIDs = new int[glyphCount];
                for (int i = 0; i < glyphCount; i++) {
                    lookupSubTable.substituteGlyphIDs[i] = data.readUnsignedShort();
                }
                lookupSubTable.coverageTable = readCoverageTable(data, offset + coverageOffset);
                return lookupSubTable;
            }
            default:
                throw new IllegalArgumentException("Unknown substFormat: " + substFormat);
        }
    }

    CoverageTable readCoverageTable(AbstractOTFDataStream data, long offset) throws IOException {
        data.seek(offset);
        int coverageFormat = data.readUnsignedShort();
        switch (coverageFormat) {
            case 1: {
                CoverageTableFormat1 coverageTable = new CoverageTableFormat1();
                coverageTable.coverageFormat = coverageFormat;
                int glyphCount = data.readUnsignedShort();
                coverageTable.glyphArray = new int[glyphCount];
                for (int i = 0; i < glyphCount; i++) {
                    coverageTable.glyphArray[i] = data.readUnsignedShort();
                }
                return coverageTable;
            }
            case 2: {
                CoverageTableFormat2 coverageTable = new CoverageTableFormat2();
                coverageTable.coverageFormat = coverageFormat;
                int rangeCount = data.readUnsignedShort();
                coverageTable.rangeRecords = new RangeRecord[rangeCount];
                for (int i = 0; i < rangeCount; i++) {
                    coverageTable.rangeRecords[i] = readRangeRecord(data);
                }
                return coverageTable;

            }
            default:
                // Should not happen (the spec indicates only format 1 and format 2)
                throw new IllegalArgumentException("Unknown coverage format: " + coverageFormat);
        }
    }

    RangeRecord readRangeRecord(AbstractOTFDataStream data) throws IOException {
        RangeRecord rangeRecord = new RangeRecord();
        rangeRecord.startGlyphID = data.readUnsignedShort();
        rangeRecord.endGlyphID = data.readUnsignedShort();
        rangeRecord.startCoverageIndex = data.readUnsignedShort();
        return rangeRecord;
    }


    private FeatureRecord[] readFeatureList(AbstractOTFDataStream data, long offset) throws IOException {
        data.seek(offset);
        int featureCount = data.readUnsignedShort();
        FeatureRecord[] featureRecords = new FeatureRecord[featureCount];
        int[] featureOffsets = new int[featureCount];
        for (int i = 0; i < featureCount; i++) {
            FeatureRecord featureRecord = new FeatureRecord();
            featureRecord.featureTag = data.readString(4);
            featureOffsets[i] = data.readUnsignedShort();
            featureRecords[i] = featureRecord;
        }
        for (int i = 0; i < featureCount; i++) {
            featureRecords[i].featureTable = readFeatureTable(data, offset + featureOffsets[i]);
        }
        return featureRecords;
    }


    FeatureTable readFeatureTable(AbstractOTFDataStream data, long offset) throws IOException {
        data.seek(offset);
        FeatureTable featureTable = new FeatureTable();
        int featureParams = data.readUnsignedShort();
        int lookupIndexCount = data.readUnsignedShort();
        featureTable.lookupListIndices = new int[lookupIndexCount];
        for (int i = 0; i < lookupIndexCount; i++) {
            featureTable.lookupListIndices[i] = data.readUnsignedShort();
        }
        return featureTable;
    }

    private LinkedHashMap<String, ScriptTable> readScriptList(AbstractOTFDataStream data, long offset) throws IOException {
        data.seek(offset);
        int scriptCount = data.readUnsignedShort();
        ScriptRecord[] scriptRecords = new ScriptRecord[scriptCount];
        int[] scriptOffsets = new int[scriptCount];
        for (int i = 0; i < scriptCount; i++) {
            ScriptRecord scriptRecord = new ScriptRecord();
            scriptRecord.scriptTag = data.readString(4);
            scriptOffsets[i] = data.readUnsignedShort();
            scriptRecords[i] = scriptRecord;
        }
        for (int i = 0; i < scriptCount; i++) {
            scriptRecords[i].scriptTable = readScriptTable(data, offset + scriptOffsets[i]);
        }
        LinkedHashMap<String, ScriptTable> resultScriptList = new LinkedHashMap<>(scriptCount);
        for (ScriptRecord scriptRecord : scriptRecords) {
            resultScriptList.put(scriptRecord.scriptTag, scriptRecord.scriptTable);
        }
        return resultScriptList;
    }

    private ScriptTable readScriptTable(AbstractOTFDataStream data, long offset) throws IOException {
        data.seek(offset);
        ScriptTable scriptTable = new ScriptTable();
        int defaultLangSys = data.readUnsignedShort();
        int langSysCount = data.readUnsignedShort();
        LangSysRecord[] langSysRecords = new LangSysRecord[langSysCount];
        int[] langSysOffsets = new int[langSysCount];
        for (int i = 0; i < langSysCount; i++) {
            LangSysRecord langSysRecord = new LangSysRecord();
            langSysRecord.langSysTag = data.readString(4);
            langSysOffsets[i] = data.readUnsignedShort();
            langSysRecords[i] = langSysRecord;
        }
        if (defaultLangSys != 0) {
            scriptTable.defaultLangSysTable = readLangSysTable(data, offset + defaultLangSys);
        }
        for (int i = 0; i < langSysCount; i++) {
            langSysRecords[i].langSysTable = readLangSysTable(data, offset + langSysOffsets[i]);
        }
        scriptTable.langSysTables = new LinkedHashMap<String, LangSysTable>(langSysCount);
        for (LangSysRecord langSysRecord : langSysRecords) {
            scriptTable.langSysTables.put(langSysRecord.langSysTag, langSysRecord.langSysTable);
        }
        return scriptTable;
    }

    LangSysTable readLangSysTable(AbstractOTFDataStream data, long l) throws IOException {
        data.seek(l);
        LangSysTable langSysTable = new LangSysTable();
        int lookupOrder = data.readUnsignedShort();
        langSysTable.requiredFeatureIndex = data.readUnsignedShort();
        int featureIndexCount = data.readUnsignedShort();
        langSysTable.featureIndices = new int[featureIndexCount];
        for (int i = 0; i < featureIndexCount; i++) {
            langSysTable.featureIndices[i] = data.readUnsignedShort();
        }
        return langSysTable;
    }


    static class ScriptRecord {
        String scriptTag;
        ScriptTable scriptTable;

        @Override
        public String toString() {
            return String.format("ScriptRecord[scriptTag=%s]", scriptTag);
        }
    }

    static class ScriptTable {
        LangSysTable defaultLangSysTable;
        LinkedHashMap<String, LangSysTable> langSysTables;

        @Override
        public String toString() {
            return String.format("ScriptTable[hasDefault=%s,langSysRecordsCount=%d]",
                    defaultLangSysTable != null, langSysTables.size());
        }
    }

    static class LangSysRecord {
        // https://www.microsoft.com/typography/otspec/languagetags.htm
        String langSysTag;
        LangSysTable langSysTable;

        @Override
        public String toString() {
            return String.format("LangSysRecord[langSysTag=%s]", langSysTag);
        }
    }

    static class LangSysTable {
        int requiredFeatureIndex;
        int[] featureIndices;

        @Override
        public String toString() {
            return String.format("LangSysTable[requiredFeatureIndex=%d]", requiredFeatureIndex);
        }
    }

    static class FeatureRecord {
        String featureTag;
        FeatureTable featureTable;

        @Override
        public String toString() {
            return String.format("FeatureRecord[featureTag=%s]", featureTag);
        }
    }

    static class FeatureTable {
        int[] lookupListIndices;

        @Override
        public String toString() {
            return String.format("FeatureTable[lookupListIndiciesCount=%d]",
                    lookupListIndices.length);
        }
    }

    static class LookupTable {
        int lookupType;
        int lookupFlag;
        int markFilteringSet;
        LookupSubTable[] subTables;

        @Override
        public String toString() {
            return String.format("LookupTable[lookupType=%d,lookupFlag=%d,markFilteringSet=%d]",
                    lookupType, lookupFlag, markFilteringSet);
        }
    }

    static abstract class LookupSubTable {
        int substFormat;
        CoverageTable coverageTable;

        abstract int doSubstitution(int gid, int coverageIndex);
    }

    static class LookupTypeSingleSubstFormat1 extends LookupSubTable {
        short deltaGlyphID;

        @Override
        int doSubstitution(int gid, int coverageIndex) {
            return coverageIndex < 0 ? gid : gid + deltaGlyphID;
        }

        @Override
        public String toString() {
            return String.format("LookupTypeSingleSubstFormat1[substFormat=%d,deltaGlyphID=%d]",
                    substFormat, deltaGlyphID);
        }
    }

    static class LookupTypeSingleSubstFormat2 extends LookupSubTable {
        int[] substituteGlyphIDs;

        @Override
        int doSubstitution(int gid, int coverageIndex) {
            return coverageIndex < 0 ? gid : substituteGlyphIDs[coverageIndex];
        }

        @Override
        public String toString() {
            return String.format(
                    "LookupTypeSingleSubstFormat2[substFormat=%d,substituteGlyphIDs=%s]",
                    substFormat, Arrays.toString(substituteGlyphIDs));
        }
    }

    static abstract class CoverageTable {
        int coverageFormat;

        abstract int getCoverageIndex(int gid);
    }

    static class CoverageTableFormat1 extends CoverageTable {
        int[] glyphArray;

        @Override
        int getCoverageIndex(int gid) {
            return Arrays.binarySearch(glyphArray, gid);
        }

        @Override
        public String toString() {
            return String.format("CoverageTableFormat1[coverageFormat=%d,glyphArray=%s]",
                    coverageFormat, Arrays.toString(glyphArray));
        }
    }

    static class CoverageTableFormat2 extends CoverageTable {
        RangeRecord[] rangeRecords;

        @Override
        int getCoverageIndex(int gid) {
            for (RangeRecord rangeRecord : rangeRecords) {
                if (rangeRecord.startGlyphID <= gid && gid <= rangeRecord.endGlyphID) {
                    return rangeRecord.startCoverageIndex + gid - rangeRecord.startGlyphID;
                }
            }
            return -1;
        }

        @Override
        public String toString() {
            return String.format("CoverageTableFormat2[coverageFormat=%d]", coverageFormat);
        }
    }

    static class RangeRecord {
        int startGlyphID;
        int endGlyphID;
        int startCoverageIndex;

        @Override
        public String toString() {
            return String.format("RangeRecord[startGlyphID=%d,endGlyphID=%d,startCoverageIndex=%d]",
                    startGlyphID, endGlyphID, startCoverageIndex);
        }
    }

    @Override
    public String toString() {
        return "AbstractWoffTable{" +
                "tag='" + tag + '\'' +
                ", offset=" + offset +
                ", compLength=" + compLength +
                ", origLength=" + origLength +
                ", origChecksum=" + origChecksum +
                ", scriptList=" + scriptList +
                ", featureList=" + Arrays.toString(featureList) +
                ", lookupList=" + Arrays.toString(lookupList) +
                ", lookupCache=" + lookupCache +
                ", reverseLookup=" + reverseLookup +
                ", lastUsedSupportedScript='" + lastUsedSupportedScript + '\'' +
                '}';
    }
}
